package edu.vt.ece.rt.benchmark.bank;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;


import edu.vt.ece.rt.benchmark.BenchmarkThread;
import edu.vt.ece.rt.dstm.Context;
import edu.vt.ece.rt.dstm.ContextDelegator;
import edu.vt.ece.rt.dstm.QObject;
import edu.vt.ece.rt.dstm.TMProxy;
import edu.vt.ece.rt.quorum.ClustersManager;
import edu.vt.ece.rt.quorum.Resource;
import edu.vt.ece.rt.quorum.util.AbortException;
import edu.vt.ece.rt.quorum.util.Names;
import edu.vt.ece.rt.util.Logger.Logger;

/**
 * @author Sudhanshu
 */

public class BankAccount extends QObject {

	private Integer amount = 0;
	private String id;

	public BankAccount(String ID) {
		id = ID;
		TMProxy.getTMProxy().publishObject(this);
	}

	public BankAccount(){}		//For cloning
	
	/**To create Account with some money at start
	 * @param id
	 * @param amount2
	 */
	public BankAccount(String ID, int amount2) {
		id = ID;
		Logger.debug("Created object with "+ID);
		amount = new Integer(amount2);		
		//		TMProxy.getTMProxy().publishObject(this);
		Resource.putQObject(getId(), this);
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public int getAmount() {
		return amount;
	}

	public void setAmount(int amt) {
		this.amount = amt;
	}


	public void deposit(int dollars) {
		amount = amount + dollars;
	}

	public boolean withdraw(int dollars) {
		amount = amount - dollars;
		return amount >= 0;
	}

	/**
	 * Provides the balance of given account
	 * 
	 * @return
	 */
	public long checkBalance() {
		return amount;
	}

	public static long totalBalanceNested(String accountNum, Context parentContext) {
		long result = 0;
		//		Logger.debug("Calculating totalBalance");

		for (int i = 0; i < 0x7fffffff; i++) {
			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
				((BenchmarkThread)Thread.currentThread()).incrementReadAborts();
			}
			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("total balance for one account");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {

				result = totalBalance(accountNum, context);

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					//					Logger.debug("Total balance is "+result);
					context.unsetParent(parentContext);
					return result;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}


	/* to be only used for sanity */
	public static long getBalance(String accountNum, Context parentContext) {

		long result = 0;
		//		Logger.debug("Calculating totalBalance");

		for (int i = 0; i < 0x7fffffff; i++) {
			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("total balance for one account");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {

				BankAccount account = (BankAccount) TMProxy.getTMProxy().read(
						accountNum, context.getContextId());

				result = account.checkBalance();

				Logger.debug("Account balance is" + account.amount);

				return result;
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}



	public static long totalBalance(String accountNum, Context c) throws Exception {

		BankAccount account = (BankAccount) TMProxy.getTMProxy().read(
				accountNum, c.getContextId());

		long balance = 0;
		for (int i = 0; i < Benchmark.calls; i++) 
			balance += account.checkBalance();

		Logger.debug("Account balance is" + account.amount);

		return balance;
	}

	public static long totalBalance(String accountNum1, String accountNum2,
			Context c) throws AbortException {

		BankAccount account1 = (BankAccount) TMProxy.getTMProxy().read(
				accountNum1, c.getContextId());
		Logger.debug("Lets check read set of one");
		Resource.displayObjects(c);

		BankAccount account2 = (BankAccount) TMProxy.getTMProxy().read(
				accountNum2, c.getContextId());
		Logger.debug("Lets check read set of two");
		Resource.displayObjects(c);

		long balance = 0;
		for (int i = 0; i < Benchmark.calls; i++) 
			balance += account1.checkBalance();

		Logger.debug("Account1 balance is" + account1.amount);

		for (int i = 0; i < Benchmark.calls; i++)
			balance += account2.checkBalance();
		Logger.debug("Account2 balance is" + account2.amount);
		return balance;

	}

	/**
	 * Get the total balance in given to accounts
	 * 
	 * @param accountNum1
	 *            - Id of bank account number 1
	 * @param accountNum2
	 *            - Id of bank account number 2
	 */
	public static long totalBalanceNested(String accountNum1, String accountNum2, Context parentContext) {
		long result = 0;
		Long priority = System.currentTimeMillis();
		Logger.debug("Calculating totalBalance for "+accountNum1 + " "+accountNum2);
		
		for (int i = 0; i < 0x7fffffff; i++) {

			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).collectStatsRoot(i);
			}

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("total balance for two accounts "+accountNum1 + " "+accountNum2);
			context.sanityNested();
			context.setPriority(priority);


			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {

				result = totalBalance(accountNum1, accountNum2, context);

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					Logger.debug("Total balance is "+result);
					return result;
				}
			} catch (AbortException e) {	
				e.touch();
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}

	/**
	 * Get the total balance in given to accounts
	 * 
	 * @param accountNum1
	 *            - Id of bank account number 1
	 * @param accountNum2
	 *            - Id of bank account number 2
	 */
	public static long rootTransaction1(String accountNum1, String accountNum2) {

		long result = 0;
		/*
		 * Explicitly setting current as null for root transaction  
		 */
		Context parentContext = null;

		for (int i = 0; i < 0x7fffffff; i++) {

			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
			}

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("starting root transaction");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {
				//				if (ClustersManager.baseClusterId ==  1 || ClustersManager.baseClusterId ==  2)
				//					Logger.debug("Do transfer for this nodes");
				transferNested(accountNum1, accountNum2, 10, context);
				transferNested(accountNum1, accountNum2, 10, context);
				transferNested(accountNum1, accountNum2, 10, context);
				//				} else {
				//					Thread.sleep(3000);
				//				}
				//				transfer(accountNum1, accountNum2, 10);
				//				BankAccount account1 = (BankAccount) TMProxy.getTMProxy().write(
				//						accountNum1, context.getContextId());
				//				BankAccount account2 = (BankAccount) TMProxy.getTMProxy().write(
				//						accountNum2, context.getContextId());
				//				Logger.debug("Money in account 1 "+account1.checkBalance());
				//				Logger.debug("Money in account 2 "+account2.checkBalance());
				totalBalanceNested(accountNum1, accountNum2, context);
				//				(accountNum1, accountNum2, 10);
				//				totalBalance(accountNum1, accountNum2);

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					Logger.debug("Nested result is "+result);
					context.unsetParent(parentContext);
					return result;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}

	/**
	 * Get the total balance in two accounts, transfer money from one to other, update the global account
	 * 
	 * @param accountNum1
	 *            - Id of bank account number 1
	 * @param accountNum2
	 *            - Id of bank account number 2
	 */
	public static long rootTransaction2(String accountNum1, String accountNum2) {

		long result = 0;
		/*
		 * Explicitly setting current as null for root transaction  
		 */
		Context parentContext = null;

		for (int i = 0; i < 0x7fffffff; i++) {
			//		for (int i = 0; i < 1; i++) {
			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
				((BenchmarkThread)Thread.currentThread()).incrementRootAborts();
				((BenchmarkThread)Thread.currentThread()).incrementWriteAborts();
				//				System.out.println("Starting root transaction on ["+ClustersManager.baseClusterId+"] for "+i+" th time");

			}

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("starting root transaction");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {
				//				Thread.sleep(500L);

				Logger.debug("Read0 value ac1="+accountNum1 + " ac2="+accountNum2);
				transfer(accountNum1, accountNum2, 10, context);


				//				totalBalanceNested(accountNum1, accountNum2, context);

				/* trivial because transfers are within banks */
				//					System.out.println("Going to update global data on ["+ClustersManager.baseClusterId+"]");
				//				Thread.sleep(800L);
				updateGlobalAccount(accountNum1, context);

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					Logger.debug("Nested result is "+result);
					context.unsetParent(parentContext);
					//					System.out.println("Committing this root transaction on ["+ClustersManager.baseClusterId+"]");
					return result;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}


	/**
	 * Transfers the given amount of money between different accounts
	 * 
	 * @param id1
	 *            - Bank Account Number 1 ID
	 * @param id2
	 *            - Bank Account Number 2 ID
	 * @param transfer
	 *            - Amount to be transferred
	 */
	public static void transferNested(String accountNum1, String accountNum2,
			int transfer, Context parentContext) throws AbortException{

		for (int i = 0; i < 0x7fffffff; i++) {

			((BenchmarkThread)Thread.currentThread()).collectStatsInner(i);

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("transfer from "+accountNum1+" account to "+accountNum2);
			context.sanityNested();

			Logger.debug("-----Started ----- W Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {
				transfer(accountNum1, accountNum2, transfer, context);
				context.mergeParent();
				return;

//				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
//					context.unsetParent(parentContext);
//					Resource.displayObjects(context);
//					return;
//				}

			} catch (AbortException e) {			
//				e.printStackTrace();
				if (e.getTransaction().longValue() != context.getContextId().longValue()) {
					Logger.debug("Exception in parent transaction "+e.getTransaction());
//					System.out.println("Exception in parent transaction "+e.getTransaction());
					throw e;
				} else {
					Logger.debug("Exception in current transaction "+e.getTransaction());
//					System.out.println("Exception in current transaction "+e.getTransaction());					
				}
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
	}

	/**
	 * Update the global account with new balances
	 * 
	 */
	public static void updateGlobalAccount(String account, Context parentContext) {


		for (int i = 0; i < 0x7fffffff; i++) {
			if (i > 0) {
				Logger.debug("global update abort");
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
				((BenchmarkThread)Thread.currentThread()).incrementInnerAborts();
				((BenchmarkThread)Thread.currentThread()).incrementWriteAborts();

				if (i > 100) {
					System.out.println("I am stuck for "+ClustersManager.baseClusterId);
				}

				try {

					Logger.debug("Sleeping for "+i*(ClustersManager.baseClusterId+1)*100L);
					Thread.sleep((ClustersManager.baseClusterId)*100L);

					Logger.debug("Waking up from sleep ["+ClustersManager.baseClusterId+"]");
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}


			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("update global account ");
			context.sanityNested();

			Logger.debug("-----Started ----- W Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {
				//				String id = (Integer.parseInt(account)%Integer.getInteger(Names.nodes))+"-global";
				String id = "global";

				BankAccount global = (BankAccount)TMProxy.getTMProxy().write(id, context.getContextId());

				global.setAmount(global.getAmount() + 0);
				//				context.mergeParent();
				//				return;
				//				updateWork(global, context);

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					context.unsetParent(parentContext);
					Resource.displayObjects(context);
					return;
				}
			} catch (Exception e) {			
				e.printStackTrace();
			}

		}
		Logger.fetal("Unable to complete transaction in given number of tries");
	}

	public static void transfer(String accountNum1, String accountNum2,
			int amount, Context c) throws AbortException {

		BankAccount account1 = (BankAccount) TMProxy.getTMProxy().write(
				accountNum1, c.getContextId());
		BankAccount account2 = (BankAccount) TMProxy.getTMProxy().write(
				accountNum2, c.getContextId());

		Logger.debug("Read value ac1="+account1.amount + " ac2="+account2.amount);
		account1.withdraw(amount);
		account2.deposit(amount);		
		Logger.debug("Written value ac1="+account1.amount + " ac2="+account2.amount);
	}

	public static void flatNestedTransfer(String accountNum1, String accountNum2) throws Exception {

		int amount = 10;
		for (int i = 0; i < 0x7fffffff; i++) {

			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
				((BenchmarkThread)Thread.currentThread()).incrementRootAborts();
			}
			Context context = ContextDelegator.getNestedContext();
			//			context.setParent(null);
			context.setDescription("nested transaction");
			context.sanityNested();

			if (i > 100) {
				//				System.out.println("I am stuck for "+ClustersManager.baseClusterId);
			}
			//
			//			try {
			//				Logger.debug("Sleeping for "+i*(ClustersManager.baseClusterId+1)*1000L);
			//				Thread.sleep(i*(ClustersManager.baseClusterId+1)*100L);
			//				Logger.debug("Waking up from sleep ["+ClustersManager.baseClusterId+"]");						
			//			} catch (InterruptedException e) {
			//				e.printStackTrace();
			//			}

			try {

				BankAccount account1 = (BankAccount) TMProxy.getTMProxy().write(accountNum1, context.getContextId());
				BankAccount account2 = (BankAccount) TMProxy.getTMProxy().write(accountNum2, context.getContextId());

				//				BankAccount account1 = (BankAccount) TMProxy.getTMProxy().read(accountNum1, context.getContextId());
				//				BankAccount account2 = (BankAccount) TMProxy.getTMProxy().read(accountNum2, context.getContextId());

				Logger.debug("Read value ac1="+account1.amount + " ac2="+account2.amount);

				account1.withdraw(amount);
				account2.deposit(amount);

				//				try {
				//
				//					Logger.debug("Sleeping for "+i*(ClustersManager.baseClusterId+1)*100L);
				//					Thread.sleep((ClustersManager.baseClusterId)*100L);
				//
				//					Logger.debug("Waking up from sleep ["+ClustersManager.baseClusterId+"]");
				//				} catch (InterruptedException e) {
				//					// TODO Auto-generated catch block
				//					e.printStackTrace();
				//				}

				//				String id1 = accountNum1.substring(0, accountNum1.indexOf('-')) + "-global";
				//								String id1 =  "global";
				//								BankAccount global1 = (BankAccount)TMProxy.getTMProxy().write(id1, context.getContextId());
				//				Thread.sleep(1000L);
				//								global1.setAmount(global1.getAmount() - 0);
				//				Thread.sleep(500L);
				//				String id2 = accountNum2.substring(0, accountNum2.indexOf('-')) + "-global";
				//				BankAccount global2 = (BankAccount)TMProxy.getTMProxy().write(id2, context.getContextId());
				//				global2.setAmount(global2.getAmount() + 10);
				//				Thread.sleep(500L);
				Logger.debug("Written value ac1="+account1.amount + " ac2="+account2.amount);
				Long start = System.currentTimeMillis();
				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					//					context.unsetParent(parentContext);
					//					Resource.displayObjects(context);
					return;
				}
			} catch (Exception e) {			

			}

		}
	}

	public static long rootTransaction3(String accountNum1, String accountNum2) {

		long result = 0;
		/*
		 * Explicitly setting current as null for root transaction  
		 */
		Context parentContext = null;

		for (int i = 0; i < 0x7fffffff; i++) {
			if (i > 0) {
				((BenchmarkThread)Thread.currentThread()).incrementAborts();
				((BenchmarkThread)Thread.currentThread()).incrementRootAborts();
				((BenchmarkThread)Thread.currentThread()).incrementWriteAborts();
				//				System.out.println("Starting root transaction on ["+ClustersManager.baseClusterId+"] for "+i+" th time");

			}

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("starting root transaction");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {


				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					Logger.debug("Nested result is "+result);
					context.unsetParent(parentContext);
					//					System.out.println("Committing this root transaction on ["+ClustersManager.baseClusterId+"]");
					return result;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
		return result;
	}


	@Override
	public String toString() {
		return getId() + "---" + hashCode();
	}

	@Override
	public QObject clone() {
		BankAccount bac = new BankAccount();
		bac.amount = amount;
		bac.id = id;
		bac.setVersion(getVersion());
		bac.setCloneOwner(getCloneOwner());
		bac.setNewObject(isNewObject());
		return bac;
	}



	public static void closedNestedTransfer(int localObjectsCount , boolean nesting) {
		Context parentContext = null;
		Long priority = System.currentTimeMillis();
		
		ArrayList<String> arr = new ArrayList<String>();
		ArrayList<Boolean> booleanVal = new ArrayList<Boolean>();
		
		if (ClustersManager.baseClusterId == 0)
			return;	

		Random random = new Random();
		for (int j = 0; j < Integer.getInteger(Names.calls); j++ ) {
			int obj1 = random.nextInt(localObjectsCount);				
			int obj2 = random.nextInt(localObjectsCount);
			while(obj2 == obj1) 
				obj2 = random.nextInt(localObjectsCount);
			String acc1 = Integer.toString(obj1);
			String acc2 = Integer.toString(obj2);
			arr.add(acc1);
			arr.add(acc2);
		}
		
		for (int i = 0; i < 0x7fffffff; i++) {

			((BenchmarkThread)Thread.currentThread()).collectStatsRoot(i);
			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("starting root transaction");
			context.sanityNested();
			context.setPriority(priority);
			
			//if (!nesting) {
			//	Random r = new Random();
//
//				try {
//					int sleepTime = r.nextInt(100)*(ClustersManager.baseClusterId + 1);
//					Thread.sleep(sleepTime);
//				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
//					e1.printStackTrace();
//				}
//
//			}

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {

				for (int j = 0; j < arr.size() - 1; j = j + 2) {
					String acc1 = arr.get(j);
					String acc2 = arr.get(j+1);
					if (nesting)
						transferNested(acc1, acc2, 10, context);
					else
						transfer(acc1, acc2, 10, context);
				}

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					context.unsetParent(parentContext);
					return;
				}
//
			}  catch (AbortException e) {
				e.touch();
				Logger.debug("Exception in root transaction "+e.getTransaction());
				Logger.debug("Exception in fart transaction "+e.getTransaction());
//				System.out.println("Exception in root transaction "+e.getTransaction());
			}
		}
	}

	
	public static void transferNested(ArrayList<String> objects,
			int transfer, Context parentContext) throws AbortException{

		for (int i = 0; i < 0x7fffffff; i++) {

			((BenchmarkThread)Thread.currentThread()).collectStatsInner(i);

			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("transfer from shuffled account");
			context.sanityNested();

			Logger.debug("-----Started ----- W Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {
				
				for (int j = objects.size()/2; j < (objects.size() - 1); j++) {
					Logger.debug("nested transfer from "+objects.get(j) +" to "+ objects.get(j+1));
					transfer(objects.get(j), objects.get(j+1), transfer, context);
				}
				context.mergeParent();
//				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
//					context.unsetParent(parentContext);
//					Resource.displayObjects(context);
//					return;
//				}
				return;
			} catch (AbortException e) {			
//				e.printStackTrace();
				if (e.getTransaction().longValue() != context.getContextId().longValue()) {
					Logger.debug("Exception in parent transaction "+e.getTransaction());
//					System.out.println("Exception in parent transaction "+e.getTransaction());
					throw e;
				} else {
					Logger.debug("Exception in current transaction "+e.getTransaction());
//					System.out.println("Exception in current transaction "+e.getTransaction());					
				}
			}
		}
		Logger.fetal("Unable to complete transaction in given number of tries");
	}

	
	public static void transferObjects(int objectsCount) {
		
		Context parentContext = null;
		ArrayList<String> objects = new ArrayList<String>();
		for (int j = 0; j < objectsCount; j++) {
			objects.add(Integer.toString(j));
		}
//		Collections.shuffle(objects);

		for (int i = 0; i < 0x7fffffff; i++) {

			((BenchmarkThread)Thread.currentThread()).collectStatsRoot(i);
			Context context = ContextDelegator.getNestedContext();
			context.setParent(parentContext);
			context.setDescription("starting root transaction");
			context.sanityNested();

			Logger.debug("----Started----- R Transaction "+context.getContextId()+" on "+ClustersManager.baseClusterId);
			try {

				
				for (int k = 0; k < ((objectsCount/2) - 1) ; k = k + 2) {
					Logger.debug("transferring from "+objects.get(k) +" to "+ objects.get(k+1));
//					transfer(objects.get(k), objects.get(k+1), 10, context);
					totalBalance(objects.get(k), objects.get(k+1), context);
				}
				
//				transferNested(objects, 10, context);
				for (int j = objects.size()/2; j < (objects.size() - 1); j++) {
					Logger.debug("nested transfer from "+objects.get(j) +" to "+ objects.get(j+1));
					transfer(objects.get(j), objects.get(j+1), 10, context);
				}
//

				if (TMProxy.getTMProxy().requestCommit(context.getContextId())) {
					context.unsetParent(parentContext);
					Resource.displayObjects(context);
					return;
				}

			}  catch (AbortException e) {
				e.touch();
				Logger.debug("Exception in root transaction "+e.getTransaction());
			}
		}
	}

}
